<template>
  <div class="song-list-wrap main-scroll-wrap" :class="type"
    ref="wrapper"
    tabindex="1"
    @keydown.delete.prevent.stop="handleDelete"
    @keydown.enter.prevent.stop="handleEnter"
    @keydown.a.prevent="handleA"
  >
    <table class="song-list-header">
      <thead>
        <tr>
          <th @click="sort('song.track')" class="track-number">#
            <i class="fa fa-angle-down" v-show="sortKey === 'song.track' && order > 0"/>
            <i class="fa fa-angle-up" v-show="sortKey === 'song.track' && order < 0"/>
          </th>
          <th @click="sort('song.title')" class="title">Title
            <i class="fa fa-angle-down" v-show="sortKey === 'song.title' && order > 0"/>
            <i class="fa fa-angle-up" v-show="sortKey === 'song.title' && order < 0"/>
          </th>
          <th @click="sort(['song.album.artist.name', 'song.album.name', 'song.track'])" class="artist">Artist
            <i class="fa fa-angle-down" v-show="sortingByArtist && order > 0"/>
            <i class="fa fa-angle-up" v-show="sortingByArtist && order < 0"/>
          </th>
          <th @click="sort(['song.album.name', 'song.track'])" class="album">Album
            <i class="fa fa-angle-down" v-show="sortingByAlbum && order > 0"/>
            <i class="fa fa-angle-up" v-show="sortingByAlbum && order < 0"/>
          </th>
          <th @click="sort('song.length')" class="time">Time
            <i class="fa fa-angle-down" v-show="sortKey === 'song.length' && order > 0"/>
            <i class="fa fa-angle-up" v-show="sortKey === 'song.length' && order < 0"/>
          </th>
          <th class="play"></th>
        </tr>
      </thead>
    </table>

    <virtual-scroller
      class="scroller"
      content-tag="table"
      :items="filteredItems"
      item-height="35"
      :renderers="renderers"
      key-field="song"
    />

    <song-menu ref="contextMenu" :songs="selectedSongs"/>
  </div>
</template>

<script>
import isMobile from 'ismobilejs'
import { each } from 'lodash'

import { filterBy, orderBy, event, pluralize, $ } from '../../utils'
import { playlistStore, queueStore, songStore, favoriteStore } from '../../stores'
import { playback } from '../../services'
import router from '../../router'
import songItem from './song-item.vue'
import songMenu from './song-menu.vue'

export default {
  name: 'song-list',
  props: ['items', 'type', 'playlist', 'sortable'],
  components: { songItem, songMenu },

  data () {
    return {
      renderers: Object.freeze({
        song: songItem
      }),
      lastSelectedRow: null,
      q: '', // The filter query
      sortKey: '',
      order: 1,
      sortingByAlbum: false,
      sortingByArtist: false,
      songRows: []
    }
  },

  watch: {
    /**
     * Watch the items.
     */
    items () {
      if (this.sortable === false) {
        this.sortKey = ''
      }

      // Update the song count and duration status on parent.
      this.$parent.updateMeta({
        songCount: this.items.length,
        totalLength: songStore.getLength(this.items, true)
      })

      this.generateSongRows()
    },

    selectedSongs (val) {
      this.$parent.setSelectedSongs(val)
    }
  },

  computed: {
    filteredItems () {
      // Allow searching specifically in title, album, and artist
      const re = /in:(title|album|artist)/ig
      const fields = []
      const matches = this.q.match(re)
      if (matches) {
        this.q = this.q.replace(re, '').trim()
        if (!this.q) {
          return this.songRows
        }
        matches.forEach(match => {
          let field = match.split(':')[1].toLowerCase()
          if (field !== 'title') {
            field = `${field}.name`
          }
          fields.push(`song.${field}`)
        })
      }

      return filterBy(
        this.songRows,
        this.q,
        ...(fields.length ? fields : ['song.title', 'song.album.name', 'song.artist.name'])
      )
    },

    /**
     * Determine if the songs in the current list can be reordered by drag-and-dropping.
     * @return {Boolean}
     */
    allowSongReordering () {
      return this.type === 'queue'
    },

    /**
     * Songs that are currently selected (their rows are highlighted).
     * @return {Array.<Object>}
     */
    selectedSongs () {
      return this.songRows.filter(row => row.selected).map(row => row.song)
    }
  },

  methods: {
    /**
     * Generate an array of "song row" or "song wrapper" objects. Since song objects themselves are
     * shared by all song lists, we can't use them directly to determine their selection status
     * (selected/unselected). Therefore, for each song list, we maintain an array of "song row"
     * objects, with each object contain the song itself, and the "selected" flag. In order to
     * comply with virtual-scroller, a "type" attribute also presents.
     */
    generateSongRows () {
      // Since this method re-generates the song wrappers, we need to keep track of  the
      // selected songs manually.
      const selectedSongIds = this.selectedSongs.map(song => song.id)

      this.songRows = this.items.map(song => {
        return {
          song,
          selected: selectedSongIds.indexOf(song.id) > -1,
          type: 'song'
        }
      })
    },

    /**
     * Handle sorting the song list.
     *
     * @param  {String} key The sort key. Can be 'title', 'album', 'artist', or 'length'
     */
    sort (key = null) {
      if (this.sortable === false) {
        return
      }

      if (key) {
        this.sortKey = key
        this.order *= -1
      }

      this.sortingByAlbum = Array.isArray(this.sortKey) && this.sortKey[0] === 'song.album.name'
      this.sortingByArtist = Array.isArray(this.sortKey) && this.sortKey[0] === 'song.album.artist.name'
      this.songRows = orderBy(this.songRows, this.sortKey, this.order)
    },

    /**
     * Execute the corresponding reaction(s) when the user presses Delete.
     */
    handleDelete () {
      if (!this.selectedSongs.length) {
        return
      }

      switch (this.type) {
        case 'queue':
          queueStore.unqueue(this.selectedSongs)
          break
        case 'favorites':
          favoriteStore.unlike(this.selectedSongs)
          break
        case 'playlist':
          playlistStore.removeSongs(this.playlist, this.selectedSongs)
          break
        default:
          break
      }

      this.clearSelection()
    },

    /**
     * Execute the corresponding reaction(s) when the user presses Enter.
     *
     * @param {Event} event The keydown event.
     */
    handleEnter (event) {
      if (!this.selectedSongs.length) {
        return
      }

      if (this.selectedSongs.length === 1) {
        // Just play the song
        playback.play(this.selectedSongs[0])
        return
      }

      switch (this.type) {
        case 'queue':
          // Play the first song selected if we're in Queue screen.
          playback.play(this.selectedSongs[0])
          break
        case 'favorites':
        case 'playlist':
        default:
          //
          // --------------------------------------------------------------------
          // For other screens, follow this map:
          //
          //  • Enter: Queue songs to bottom
          //  • Shift+Enter: Queues song to top
          //  • Cmd/Ctrl+Enter: Queues song to bottom and play the first selected song
          //  • Cmd/Ctrl+Shift+Enter: Queue songs to top and play the first queued song
          //
          // Also, if there's only one song selected, play it right away.
          // --------------------------------------------------------------------
          //
          queueStore.queue(this.selectedSongs, false, event.shiftKey)

          this.$nextTick(() => {
            router.go('queue')

            if (event.ctrlKey || event.metaKey || this.selectedSongs.length === 1) {
              playback.play(this.selectedSongs[0])
            }
          })

          break
      }
    },

    /**
     * Capture A keydown event and select all if applicable.
     *
     * @param {Event} event The keydown event.
     */
    handleA (event) {
      if (!event.metaKey && !event.ctrlKey) {
        return
      }

      this.selectAllRows()
    },

    /**
     * Select all rows in the current list.
     */
    selectAllRows () {
      each(this.songRows, row => {
        row.selected = true
      })
    },

    /**
     * Handle the click event on a row to perform selection.
     *
     * @param  {VueComponent} rowVm
     * @param  {Event} e
     */
    rowClicked (rowVm, event) {
      // If we're on a touch device, or if Ctrl/Cmd key is pressed, just toggle selection.
      if (isMobile.any) {
        this.toggleRow(rowVm)
        return
      }

      if (event.ctrlKey || event.metaKey) {
        this.toggleRow(rowVm)
      }

      if (event.button === 0) {
        if (!(event.ctrlKey || event.metaKey || event.shiftKey)) {
          this.clearSelection()
          this.toggleRow(rowVm)
        }

        if (event.shiftKey && this.lastSelectedRow) {
          this.selectRowsBetween(this.lastSelectedRow, rowVm)
        }
      }
    },

    /**
     * Toggle select/unslect a row.
     *
     * @param  {VueComponent} rowVm The song-item component
     */
    toggleRow (rowVm) {
      rowVm.item.selected = !rowVm.item.selected
      this.lastSelectedRow = rowVm
    },

    /**
     * Select all rows between two rows.
     *
     * @param  {VueComponent} firstRowVm  The first row's component
     * @param  {VueComponent} secondRowVm The second row's component
     */
    selectRowsBetween (firstRowVm, secondRowVm) {
      const indexes = [this.songRows.indexOf(firstRowVm.item), this.songRows.indexOf(secondRowVm.item)]
      indexes.sort((a, b) => a - b)

      for (let i = indexes[0]; i <= indexes[1]; ++i) {
        this.songRows[i].selected = true
      }
    },

    /**
     * Clear the current selection on this song list.
     */
    clearSelection () {
      each(this.songRows, row => {
        row.selected = false
      })
    },

    /**
     * Enable dragging songs by capturing the dragstart event on a table row.
     * Even though the event is triggered on one row only, we'll collect other
     * selected rows, if any, as well.
     *
     * @param {VueComponent} The row's Vue component
     * @param {Event} event The event
     */
    dragStart (rowVm, event) {
      // If the user is dragging an unselected row, clear the current selection.
      if (!rowVm.item.selected) {
        this.clearSelection()
        rowVm.item.selected = true
      }

      this.$nextTick(() => {
        const songIds = this.selectedSongs.map(song => song.id)
        event.dataTransfer.setData('application/x-koel.text+plain', songIds)
        event.dataTransfer.effectAllowed = 'move'

        // Set a fancy drop image using our ghost element.
        const ghost = document.getElementById('dragGhost')
        ghost.innerText = `${pluralize(songIds.length, 'song')}`
        event.dataTransfer.setDragImage(ghost, 0, 0)
      })
    },

    /**
     * Add a "droppable" class and set the drop effect when other songs are dragged over a row.
     *
     * @param {Event} event The dragover event.
     */
    allowDrop (event) {
      if (!this.allowSongReordering) {
        return
      }

      $.addClass(event.target.parentNode, 'droppable')
      event.dataTransfer.dropEffect = 'move'

      return false
    },

    /**
     * Perform reordering songs upon dropping if the current song list is of type Queue.
     *
     * @param  {VueComponent} rowVm The row's Vue Component
     * @param  {Event} event
     */
    handleDrop (rowVm, event) {
      if (
        !this.allowSongReordering ||
        !event.dataTransfer.getData('application/x-koel.text+plain') ||
        !this.selectedSongs.length
      ) {
        return this.removeDroppableState(event)
      }

      queueStore.move(this.selectedSongs, rowVm.song)

      return this.removeDroppableState(event)
    },

    /**
     * Remove the droppable state (and the styles) from a row.
     *
     * @param  {Event} event
     */
    removeDroppableState (event) {
      $.removeClass(event.target.parentNode, 'droppable')
      return false
    },

    /**
     * Open the context menu.
     *
     * @param  {VueComponent} rowVm The right-clicked row's component
     * @param  {Event} event
     */
    openContextMenu (rowVm, event) {
      // If the user is right-clicking an unselected row,
      // clear the current selection and select it instead.
      if (!rowVm.item.selected) {
        this.clearSelection()
        this.toggleRow(rowVm)
      }

      this.$nextTick(() => this.$refs.contextMenu.open(event.pageY, event.pageX))
    }
  },

  created () {
    event.on({
      /**
       * Listen to 'filter:changed' event to filter the current list.
       */
      'filter:changed': q => {
        this.q = q
      }
    })
  }
}
</script>

<style lang="scss">
@import "../../../sass/partials/_vars.scss";
@import "../../../sass/partials/_mixins.scss";

.song-list-wrap {
  position: relative;
  padding: 8px 24px;

  .song-list-header {
    position: absolute;
    top: 0;
    left: 24px;
    right: 24px;
    padding: 0 24px;
    background: #1b1b1b;
    z-index: 1;
    width: calc(100% - 48px);
  }

  table {
    width: 100%;
    table-layout: fixed;
  }

  tr.droppable {
    border-bottom-width: 3px;
    border-bottom-color: $colorGreen;
  }

  td, th {
    text-align: left;
    padding: 8px;
    vertical-align: middle;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;

    &.time {
      width: 72px;
      text-align: right;
    }

    &.track-number {
      width: 42px;
    }

    &.artist {
      width: 23%;
    }

    &.album {
      width: 27%;
    }

    &.play {
      display: none;

      html.touchevents & {
        display: block;
        position: absolute;
        top: 8px;
        right: 4px;
      }
    }
  }

  th {
    color: $color2ndText;
    letter-spacing: 1px;
    text-transform: uppercase;
    cursor: pointer;

    i {
      color: $colorHighlight;
      font-size: 1.2rem;
    }
  }

  /**
   * Since the Queue screen doesn't allow sorting, we reset the cursor style.
   */
  &.queue th {
    cursor: default;
  }

  .scroller {
    overflow: auto;
    position: absolute;
    top: 35px;
    left: 0;
    bottom: 0;
    right: 0;
    overflow-y: scroll;
    -webkit-overflow-scrolling: touch;

    .item-container {
      position: absolute;
      left: 24px;
      right: 24px;
    }

    .item {
      margin-bottom: 0;
    }
  }

  @media only screen and (max-width: 768px) {
    table, tbody, tr {
      display: block;
    }

    thead, tfoot {
      display: none;
    }

    .scroller {
      top: 0;
      bottom: 24px;

      .item-container {
        left: 12px;
        right: 12px;
      }
    }

    tr {
      padding: 8px 32px 8px 4px;
      position: relative;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      color: $color2ndText;
      width: 100%;
    }

    td {
      display: inline;
      padding: 0;
      vertical-align: bottom;
      color: $colorMainText;

      &.album, &.time, &.track-number {
        display: none;
      }

      &.artist {
        color: $color2ndText;
        font-size: .9rem;
        padding: 0 4px;
      }
    }
  }
}
</style>
